package com.hwd.gmall.list.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.hwd.gmall.common.constant.ElasticSearchConst;
import com.hwd.gmall.list.service.SearchService;
import com.hwd.gmall.model.list.Goods;
import com.hwd.gmall.model.list.SearchResponseAttrVo;
import com.hwd.gmall.model.list.SearchResponseTmVo;
import lombok.extern.log4j.Log4j2;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author 黄伟东/Victor
 * @date 2022/4/28 11:08
 */
@Service
@Log4j2
public class SearchServiceImpl implements SearchService {

    @Resource
    private RestHighLevelClient restHighLevelClient;

    /**
     * 根据搜索关键词查询商品
     *
     * @param searchData 搜索关键字
     * @return java.util.Map<java.lang.String, java.lang.Object>
     */
    @Override
    public Map<String, Object> searchGoods(Map<String, String> searchData) {

        // 构建查询条件
        SearchRequest searchRequest = buildQueryParam(searchData);

        try {

            // 执行查询
            SearchResponse searchResponse =
                    restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            // 解析返回结果
            return getSearchResult(searchResponse);

        } catch (IOException e) {
            log.error("执行es查询失败，报错信息如下:" + e.getMessage());
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 构建查询条件
     *
     * @param searchData 搜索关键词
     * @return org.elasticsearch.action.search.SearchRequest
     */
    private SearchRequest buildQueryParam(Map<String, String> searchData) {

        // 初始化搜索条件构造器
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // 初始化组合查询条件构造器
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 关键词条件设置
        mustKeywordsQuery(searchData, boolQueryBuilder);

        // 品牌条件设置，前端传参格式：tradeMark=1:华为
        mustTradeMarkQuery(searchData, boolQueryBuilder);

        // 平台属性的条件设置(多个平台属性)
        mustAttrsQuery(searchData, boolQueryBuilder);

        // 价格查询条件设置
        mustPriceQuery(searchData, boolQueryBuilder);

        // 设置排序条件，获取排序参数
        addSort(searchData, searchSourceBuilder);

        // 设置分页条件
        addPage(searchData, searchSourceBuilder);

        // 设置查询的条件
        searchSourceBuilder.query(boolQueryBuilder);

        // 品牌聚合查询条件设置
        addTradeMarkAggregation(searchSourceBuilder);

        // 平台属性聚合查询条件设置
        addAttrsAggregation(searchSourceBuilder);

        // 高亮条件设置
        addHighlighter(searchSourceBuilder);


        // 返回构建好查询条件请求对象
        return new SearchRequest("goods").source(searchSourceBuilder);
    }

    /**
     * 高亮条件设置
     *
     * @param searchSourceBuilder 搜索条件构造器
     */
    private void addHighlighter(SearchSourceBuilder searchSourceBuilder) {
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");
        highlightBuilder.preTags("<font style=color:red>");
        highlightBuilder.postTags("</font>");
        searchSourceBuilder.highlighter(highlightBuilder);
    }

    /**
     * 平台属性聚合查询条件设置
     *
     * @param searchSourceBuilder 搜索条件构造器
     */
    private void addAttrsAggregation(SearchSourceBuilder searchSourceBuilder) {
        searchSourceBuilder.aggregation(
                AggregationBuilders.nested("aggAttrs", "attrs")
                        .subAggregation(
                                AggregationBuilders.terms("aggAttrId").field("attrs.attrId")
                                        .subAggregation(AggregationBuilders.terms("aggAttrName").field("attrs.attrName"))
                                        .subAggregation(AggregationBuilders.terms("aggAttrValue").field("attrs.attrValue"))
                                        .size(100)
                        )
        );
    }

    /**
     * 品牌聚合查询条件设置
     *
     * @param searchSourceBuilder 搜索条件构造器
     */
    private void addTradeMarkAggregation(SearchSourceBuilder searchSourceBuilder) {
        searchSourceBuilder.aggregation(
                AggregationBuilders.terms("aggTmId").field("tmId")
                        .subAggregation(AggregationBuilders.terms("aggTmName").field("tmName"))
                        .subAggregation(AggregationBuilders.terms("aggTmLogoUrl").field("tmLogoUrl"))
                        .size(100)
        );
    }

    /**
     * 设置关键词查询条件
     *
     * @param searchData       查询条件
     * @param boolQueryBuilder 组合条件查询构造器
     */
    private void mustKeywordsQuery(Map<String, String> searchData, BoolQueryBuilder boolQueryBuilder) {
        String keywords = searchData.get("keywords");
        if (!StringUtils.isEmpty(keywords)) {
            boolQueryBuilder.must(QueryBuilders.matchQuery("title", keywords));
        }
    }

    /**
     * 设置分页查询条件
     *
     * @param searchData          查询条件
     * @param searchSourceBuilder 组合条件查询构造器
     */
    private void addPage(Map<String, String> searchData, SearchSourceBuilder searchSourceBuilder) {
        // 设置分页条件: 固定每页显示100条数据
        String size = searchData.get("size");
        searchSourceBuilder.size(getSize(size));

        // 设置页码
        String page = searchData.get("page");
        searchSourceBuilder.from((getPage(page) - 1) * getSize(size));
    }

    /**
     * 设置排序条件
     *
     * @param searchData          查询条件
     * @param searchSourceBuilder 组合条件查询构造器
     */
    private void addSort(Map<String, String> searchData, SearchSourceBuilder searchSourceBuilder) {
        String sortField = searchData.get("sortField");
        String sortRule = searchData.get("sortRule");

        // 获取用户传入的排序规则
        SortOrder sortOrder = getSortOrder(sortRule);

        if (!StringUtils.isEmpty(sortField) && !StringUtils.isEmpty(sortRule) && sortOrder != null) {
            searchSourceBuilder.sort(sortField, sortOrder);
        } else {
            // 默认排序
            searchSourceBuilder.sort("id", SortOrder.DESC);
        }
    }

    /**
     * 获取用户传入的排序规则
     *
     * @param sortRule 排序规则
     * @return org.elasticsearch.search.sort.SortOrder
     */
    private SortOrder getSortOrder(String sortRule) {
        // 升序
        if (ElasticSearchConst.SORT_ASC.equalsIgnoreCase(sortRule)) {
            return SortOrder.ASC;
        }
        // 降序
        if (ElasticSearchConst.SORT_DESC.equalsIgnoreCase(sortRule)) {
            return SortOrder.DESC;
        }
        return null;
    }

    /**
     * 拼接价格查询条件
     *
     * @param searchData       查询条件
     * @param boolQueryBuilder 组合条件查询构造器
     */
    private void mustPriceQuery(Map<String, String> searchData, BoolQueryBuilder boolQueryBuilder) {
        // 价格查询条件设置，前端传参格式：price=2000-3000元，3000元以上
        String price = searchData.get("price");
        if (!StringUtils.isEmpty(price)) {
            // 去掉汉字，price=2000-3000 ， 3000
            price = price.replace("元", "").replace("以上", "");
            // 切分：2000 3000 ，3000
            String[] split = price.split("-");
            if (split.length > 0 && isLong(split[0])) {
                // 第一个数字
                boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gte(split[0]));
                // 如果有第二个数字
                if (split.length > 1) {
                    boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(split[1]));
                }
            }
        }
    }

    /**
     * 拼接平台属性查询条件
     *
     * @param searchData       查询条件
     * @param boolQueryBuilder 组合条件查询构造器
     */
    private void mustAttrsQuery(Map<String, String> searchData, BoolQueryBuilder boolQueryBuilder) {
        // 平台属性的条件设置(多个平台属性)，前端传参格式：attr_平台属性名=平台属性id:平台属性值
        // 先过滤符合平台属性传参要求的参数，前缀为attr_
        searchData.entrySet().stream()
                .filter(entry -> entry.getKey().startsWith("attr_"))
                .forEach(entry -> {
                    // 获取平台属性值：1:红色
                    String[] strings = entry.getValue().split(":");
                    if (strings.length > 1 && isLong(strings[0])) {
                        /*
                         * 1.查询的域的名字
                         * 2.查询的条件: 平台属性id等于用户传递的,平台属性的值等于用户传递的
                         */
                        boolQueryBuilder.must(QueryBuilders.nestedQuery("attrs",
                                QueryBuilders.boolQuery()
                                        .must(QueryBuilders.termQuery("attrs.attrId", strings[0]))
                                        .must(QueryBuilders.termQuery("attrs.attrValue", strings[1])),
                                ScoreMode.None));
                    }
                });
    }

    /**
     * 拼接品牌查询条件
     *
     * @param searchData       查询条件
     * @param boolQueryBuilder 组合条件查询构造器
     */
    private void mustTradeMarkQuery(Map<String, String> searchData, BoolQueryBuilder boolQueryBuilder) {

        // 品牌条件设置，前端传参格式：tradeMark=1:华为
        String tradeMark = searchData.get("tradeMark");

        if (!StringUtils.isEmpty(tradeMark)) {

            String[] tradeMarkSplit = tradeMark.split(":");

            String tmId = tradeMarkSplit[0];

            // 判断字符串合法性
            if (isLong(tmId)) {
                boolQueryBuilder.must(QueryBuilders.termQuery("tmId", tmId));
            }

        }

    }

    /**
     * 判断字符串是否为Long型id
     *
     * @param str 字符串
     * @return boolean
     */
    private boolean isLong(String str) {
        try {
            return new Long(str) >= 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }


    /**
     * 获取用户传入的每页显示条数
     *
     * @param size 每页显示条数
     * @return java.lang.Integer
     */
    private Integer getSize(String size) {
        try {
            int i = Integer.parseInt(size);
            return i > 0 ? i : 5;
        } catch (NumberFormatException e) {
            return 5;
        }
    }

    /**
     * 获取用户传入的页码
     *
     * @param page 页码
     * @return java.lang.Integer
     */
    private Integer getPage(String page) {
        try {
            int i = Integer.parseInt(page);
            return i > 0 ? i : 1;
        } catch (NumberFormatException e) {
            return 1;
        }
    }

    /**
     * 解析搜索结果
     *
     * @param searchResponse 搜索结果对象
     * @return java.util.Map<java.lang.String, java.lang.Object>
     */
    private Map<String, Object> getSearchResult(SearchResponse searchResponse) {

        // 初始化返回结果对象
        Map<String, Object> result = new HashMap<>(16);

        // 获取命中数据
        SearchHit[] searchHits = searchResponse.getHits().getHits();

        // 获取符合搜索条件的总数量
        long totalHits = searchResponse.getHits().getTotalHits();
        result.put("totalHits",totalHits);

        // 获取商品商品的数据，数据反序列化
        List<Goods> goodsList = getGoodsList(searchHits);
        result.put("goodsList", goodsList);

        // 获取聚合查询的结果
        Aggregations aggregations = searchResponse.getAggregations();

        // 解析品牌的聚合结果
        List<SearchResponseTmVo> searchResponseTmVoList = getTradeMarkAggResult(aggregations);
        result.put("searchResponseTmVoList", searchResponseTmVoList);

        // 解析平台属性的聚合查询结果
        List<SearchResponseAttrVo> searchResponseAttrVoList = getAttrInfoAggResult(aggregations);
        result.put("searchResponseAttrVoList", searchResponseAttrVoList);

        // 返回解析后的搜索结果
        return result;
    }

    /**
     * 获取商品的数据
     *
     * @param searchHits 搜索命中的数据
     * @return java.util.List<com.hwd.gmall.model.list.Goods>
     */
    private List<Goods> getGoodsList(SearchHit[] searchHits) {
        return Arrays.stream(searchHits).map(hit -> {
            // 反序列化
            Goods goods = JSONObject.parseObject(hit.getSourceAsString(), Goods.class);
            // 获取并保存高亮的数据
            saveHighlightData(hit, goods);
            return goods;
        }).collect(Collectors.toList());
    }

    /**
     * 获取高亮数据
     *
     * @param hit   命中的数据
     * @param goods 用于保存高亮数据的商品对象
     */
    private void saveHighlightData(SearchHit hit, Goods goods) {

        HighlightField highlightField = hit.getHighlightFields().get("title");

        if (highlightField != null) {

            // 获取全部高亮数据
            Text[] fragments = highlightField.getFragments();

            // 数据非空判断
            if (fragments != null && fragments.length > 0) {

                // 拼接高亮数据
                StringBuilder title = new StringBuilder();

                for (Text fragment : fragments) {
                    title.append(fragment);
                }

                // 替换高亮数据
                goods.setTitle(title.toString());

            }

        }

    }

    /**
     * 获取平台属性聚合查询的结果
     *
     * @param aggregations 查询的聚合结果
     * @return java.util.List<com.hwd.gmall.model.list.SearchResponseAttrVo>
     */
    private List<SearchResponseAttrVo> getAttrInfoAggResult(Aggregations aggregations) {

        // 获取平台属性nested类型的聚合结果
        ParsedNested aggAttrs = aggregations.get("aggAttrs");

        // 获取attrs子聚合的结果，平台属性id聚合结果
        ParsedLongTerms aggAttrId = aggAttrs.getAggregations().get("aggAttrId");

        // 获取每个平台属性id的名字和值列表
        return aggAttrId.getBuckets().stream().map(bucket -> {

            // 初始化平台属性数据对象
            SearchResponseAttrVo searchResponseAttrVo = new SearchResponseAttrVo();

            // 获取平台属性id
            long attrId = bucket.getKeyAsNumber().longValue();
            searchResponseAttrVo.setAttrId(attrId);

            // 获取子聚合查询的结果，平台属性名称
            ParsedStringTerms aggAttrName = bucket.getAggregations().get("aggAttrName");
            if (!aggAttrName.getBuckets().isEmpty()) {
                String attrName = aggAttrName.getBuckets().get(0).getKeyAsString();
                searchResponseAttrVo.setAttrName(attrName);
            }

            // 获取子聚合查询的结果，平台属性值
            ParsedStringTerms aggAttrValue = bucket.getAggregations().get("aggAttrValue");
            if (!aggAttrValue.getBuckets().isEmpty()) {
                List<String> attrValueList = aggAttrValue.getBuckets().stream()
                        .map(MultiBucketsAggregation.Bucket::getKeyAsString)
                        .collect(Collectors.toList());
                searchResponseAttrVo.setAttrValueList(attrValueList);
            }

            // 返回平台属性数据
            return searchResponseAttrVo;

        }).collect(Collectors.toList());
    }

    /**
     * 获取品牌的聚合结果
     *
     * @param aggregations 查询的聚合结果
     * @return java.util.List<com.hwd.gmall.model.list.SearchResponseTmVo>
     */
    private List<SearchResponseTmVo> getTradeMarkAggResult(Aggregations aggregations) {

        // 获取品牌id聚合查询的结果
        ParsedLongTerms aggTmId = aggregations.get("aggTmId");

        return aggTmId.getBuckets().stream().map(bucket -> {

            // 初始化品牌数据对象
            SearchResponseTmVo searchResponseTmVo = new SearchResponseTmVo();

            // 获取品牌id
            long tmId = bucket.getKeyAsNumber().longValue();
            searchResponseTmVo.setTmId(tmId);

            // 获取子聚合查询的结果，品牌名称
            ParsedStringTerms aggTmName = bucket.getAggregations().get("aggTmName");
            if (!aggTmName.getBuckets().isEmpty()) {
                String tmName = aggTmName.getBuckets().get(0).getKeyAsString();
                searchResponseTmVo.setTmName(tmName);
            }

            // 获取子聚合查询的结果，品牌logo
            ParsedStringTerms aggTmLogoUrl = bucket.getAggregations().get("aggTmLogoUrl");
            if (!aggTmLogoUrl.getBuckets().isEmpty()) {
                String tmLogoUrl = aggTmLogoUrl.getBuckets().get(0).getKeyAsString();
                searchResponseTmVo.setTmLogoUrl(tmLogoUrl);
            }

            // 返回查询到的品牌数据
            return searchResponseTmVo;

        }).collect(Collectors.toList());
    }
}
